<template>
  <div class="DataBindView">
    <div class="daText">{{msg}}</div>
    <br />
    <div class="csdesc">
      1.Vue的双向绑定实现原理：由<strong>数据劫持</strong>结合<strong>发布者-订阅者模式</strong>实现的。<br />
      数据劫持：通过Object.defineProperty()来劫持对象属性的setter和getter操作，在数据变动的时候做想做的事情。
    </div>
    <img src="../assets/img/1353113-20180911112243151-1244500483.png" alt="Object.defineProperty">
    <div class="code">
      <pre>
    // 常用方式
    var Book = {
      name: '人性的弱点'
    };
    console.log(Book.name);  // 人性的弱点

    // 通过Object.defineProperty方式，在console.log(book.name)同时,直接给书加一个书号
    var Book = {};
    var name = '';
    Object.defineProperty(Book,'name',{
      set:function(value) {
        name = value;
        console.log('你取了一个书名叫:' + value);
      },
      get:function() {
        console.log('get方法被监听到');
        return name;
      }
    });
    Book.name = '人性的弱点'; // 你取了一个书名叫:人性的弱点
    console.log(Book.name); // ①get方法被监听到   ②人性的弱点
      </pre>
    </div>
    <div class="csdesc">
      通过<strong>Object.defineProperty()</strong>这个方法设置了Book对象的name属性，对其get和set方法进行重写操作，get方法在获得name属性时被调用，set方法在设置name属性时被触发。
      所以在执行Book.name="人性的弱点"，这个语句时调用set方法，输出你取了一个书名叫：人性的弱点．当调用console.log(Book.name)时触发get方法，输出人性的弱点。
    </div>
    <div class="csdesc">
      订阅者和发布者模式，通常用于消息队列中．一般有<strong>两种形式来实现消息队列</strong>：<br />
      ①使用<code>生产者和消费者</code>来实现；<br />
      ②使用<code>订阅者－发布者</code>模式来实现；<br />
      其中订阅者和发布者实现消息队列的方式，就会用订阅者模式。
    </div>
    <div class="csdesc">
      所谓的订阅者，就像我们在日常生活中，订阅报纸一样。我们订阅报纸的时候，通常都得需要在报社或者一些中介机构进行注册。
      当有新版的报纸发刊的时候，邮递员就需要向订阅该报纸的人，依次发放报纸。
    </div>
    <div class="csdesc">
      如果用代码实现该模式，需要进行两个步骤：<br />
      ①、初始化发布者、订阅者<br />
      ②、订阅者需要注册到发布者，发布者发布消息时，依次向订阅者发布消息。
    </div>
    <div class="csdesc">
      要想实现mvvm，主要包含两个方面，视图变化更新数据，数据变化更新视图．<br />
      data变化更新view的重点是如何知道view什么时候变化了，只要知道什么时候view变化了，那么接下来的就好处理了．
      这个时候我们上文提到的Object.defineProperty( )就起作用了．通过Object.defineProperty( )对属性设置一个set函数，当属性变化时就会触发这个函数，
      所以我们只需要将一些更新的方法放在set函数中就可以实现data变化更新view了．<br /><br />

      首先要对数据进行劫持监听，所以我们首先要设置一个<code>监听器Observer</code>,用来监听所有的属性，当属性变化时，就需要通知<code>订阅者Watcher</code>,看是否需要更新．
      因为属性可能是多个，所以会有多个订阅者，故我们需要一个消息订阅器Dep来专门收集这些订阅者，并在监听器Observer和订阅者Watcher之间进行统一的管理．
      以为在节点元素上可能存在一些指令，所以我们还需要有一个<code>指令解析器Compile</code>，对每个节点元素进行扫描和解析，将相关指令初始化成一个订阅者Watcher，
      并替换模板数据并绑定相应的函数，这时候当订阅者Watcher接受到相应属性的变化，就会执行相对应的更新函数，从而更新视图．<br />
      <br />
      <strong>实现过程</strong><br />
      1.实现一个监听器Observer，用来劫持并监听所有属性，如果有变动的，就通知订阅者。<br />
      2.实现一个订阅者Watcher，可以收到属性的变化通知并执行相应的函数，从而更新视图。<br />
      3.实现一个解析器Compile，可以扫描和解析每个节点的相关指令，并根据初始化模板数据以及初始化相应的订阅器。<br />
    </div>
    <div class="csdesc">
      <strong>1.实现监听器Observer</strong><br />
      数据监听器的核心方法就是Object.defineProperty()，通过遍历循环对所有属性值进行监听，并对其进行Object.defineProperty()处理。
    </div>
    <div class="code">
      <pre>
    // 对所有的属性都要进行监听，递归遍历所有属性
    function defineReactive (data, key, val) {
      observe(val); // 递归遍历所有的属性
      <span class="havey">Object.defineProperty(data, key, {
        enumerable: true, // 当且仅当该属性的 configurable 为true时，该属性描述符才能够被改变，同时改属性也能从对象上被删除。
        configurable: true, // 当且仅当该属性的enumerable为true时，该属性才能够出现在对象的枚举属性中。
        get: function() {
          return val;
        },
        set: function(newVal) {
          val = newVal;
          console.log('属性'+key+'已经被监听,现在值为:"'+newVal.toString()+'"');
        }
      });</span>
    }

    function observe(data){
      if (!data || typeof data !== 'object') {
        return
      }
      <span class="havey">Object.keys(data).forEach(function(key){
        defineReactive(data, key, data[key])
      })</span>
    }

    var library = {
      book1: {
        name: ''
      },
      book2: ''
    };
    observe(library);
    library.book1.name = 'vue权威指南';
    // 属性name已经被监听,现在值为:"vue权威指南"
    // "vue权威指南"

    library.book2 = '没有书籍';
    // 属性book2已经被监听,现在值为:"没有此书"
    // "没有此书"
      </pre>
    </div>
    <div class="csesc">
      在上面的思路中，我们需要一个可以容纳消息订阅者的消息订阅器Dep，订阅器主要收集消息订阅者，然后在属性变化时执行相应订阅者的更新函数，那么消息订阅器Dep需要有一个容器，
      用来存放消息订阅者．我们将上面的监听器Observer稍微修改一下：
    </div>
    <div class="code">
      <pre>
    function defineReactive (data, key, val) {
      observe(val);
      var dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          <span class="havey">if(是否需要添加订阅者) { // Watcher初始化出发
            dep.addSub(watcher); // 在这里添加一个订阅者
          }</span>
          return val;
        },
        set: function(newVal) {
          if (val == newVal) {
            return;
          }
          val = newVal;
          console.log('属性'+key+'已经被监听,现在值为:"'+newVal.toString()+'"');
          <span class="havey">dep.notify(); // 如果数据变化，就通知所有的订阅者</span>
        }
      });
    }

    function observe(data){
      if (!data || typeof data !== 'object') {
        return
      }
      Object.keys(data).forEach(function(key){
        defineReactive(data, key, data[key])
      })
    }

    <span class="havey">function Dep() {
      this.subs = [];
    }

    // prototype 属性使您有能力向对象添加属性和方法，这个属性只有函数对象才有，
    // 具体的说就是构造函数具有.只要你声明定义了一个函数对象，这个prototype就会存在

    Dep.prototype = {
      addSub: function (sub) {
        this.subs.push(sub);
      },
      notify: function () {
        this.subs.forEach(function(sub){
          sub.update(); // 通知每个订阅者检查更新
        })
      }
    }

    Dep.target = null;</span>
      </pre>
    </div>
    <div class="csdesc">
      在代码中，我们将订阅器Dep添加一个订阅者设计在get里面，这是为了让Watcher在初始化时触发，因此判断是否需要需要添加订阅者，至于具体实现的方法，我们在下文中深究．
      在set方法中，如果函数变化，就会通知所有的订阅者，订阅者们将会执行相对应的更新函数，到目前为止，一个比较完善的Observer已经成型了，下面我们要写订阅者Watcher.
    </div>
    <div class="csdesc">
      <strong>上述代码中就是数据绑定中最核心的地方，更详细的代码请阅览：<a href="https://github.com/Aventury/MVVM" target="_blank">mvvm</a></strong>
    </div>
  </div>
</template>

<script>
export default {
  name: 'DataBindView',
  data () {
    return {
      msg: '这里是data里的msg，在模板中使用{{msg}}，就能看到数据的变化。'
    }
  }
}
</script>
<style lang="less" scoped>
.DataBindView {
  .daText {
    margin-top: 10px;
    color: rgb(218, 86, 86);
    font-size: 16px;
    line-height: 20px;
  }
}
</style>
